/* CsJni - .NET / Java Bridge
 * 
 * Copyright 2010 Robert E. Burke Licensed under the Apache License, Version 
 * 2.0 (the "License"); you may not use this file except in compliance with 
 * the License. You may obtain a copy of the License at 
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable 
 * law or agreed to in writing, software distributed under the License is 
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
 * KIND, either express or implied. See the License for the specific language 
 * governing permissions and limitations under the License. 
*/

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Globalization;

namespace CsJni
{
    // http://www.bluebytesoftware.com/blog/PermaLink,guid,2d0038b5-7ba5-421f-860b-d9282a1211d3.aspx
    // http://msdn.microsoft.com/msdnmag/issues/05/08/Concurrency/
    internal sealed class JniEnvironmentCache
    {
        private const string JNIENV_TLS_NAME = "JNIEnvSlot";

        private Dictionary<IntPtr, WeakReference> mMapEnvironments = new Dictionary<IntPtr, WeakReference>();
        private int mLastManagedThreadId = -1;
        private WeakReference mLastJniEnv;

        public JniEnvironmentCache(IntPtr mainThreadEnvironmentHandle)
        {
            if (Thread.GetNamedDataSlot(JNIENV_TLS_NAME) == null)
            {
                Thread.AllocateNamedDataSlot(JNIENV_TLS_NAME);
            }
            AddEnvironment(new JniEnvironment(true, mainThreadEnvironmentHandle, CurrentUnmanagedThreadId));
        }

        private static int CurrentUnmanagedThreadId
        {
            get { return Thread.CurrentThread.ManagedThreadId; } // TODO
        }

        public static bool CheckThreadAffinity(JniEnvironment environment)
        {
            return (environment.ThreadAffinity == JniEnvironmentCache.CurrentUnmanagedThreadId);
        }

        public JniEnvironment FromHandle(IntPtr environmentHandle)
        {
            lock (mMapEnvironments)
            {
                JniEnvironment env = FromHandleCore(environmentHandle);
                if (env == null)
                {
                    env = new JniEnvironment(false, environmentHandle, CurrentUnmanagedThreadId);
                    AddEnvironment(env);
                }
                return env;
            }
        }

        internal JniEnvironment GetEnvironment(out bool isNew)
        {
            // NOTE: this method has been optimized for speed.  On my machine, calling 
            // JavaVM.Platform.FindEnvironment() (which calls this method) 10,000,000 times in 
            // a loop took 2.562 seconds (in other words, can call 3,900,000 times per second).
            lock (this)
            {
                if (mLastManagedThreadId == Thread.CurrentThread.ManagedThreadId)
                {
                    try
                    {
                        JniEnvironment lastEnv = (JniEnvironment)mLastJniEnv.Target;
                        isNew = false;
                        return lastEnv;
                    }
                    catch (InvalidOperationException)
                    {
                        // eat it
                    }
                }
                int unmanagedThreadId = 0;
                JniEnvironment env = (JniEnvironment)Thread.GetData(Thread.GetNamedDataSlot(JNIENV_TLS_NAME));
                if (env == null)
                {
                    IntPtr handle = GetEnvironmentHandle(out unmanagedThreadId, out isNew);
                    env = FromHandle(handle);
                    if (env == null)
                    {
                        env = new JniEnvironment(false, handle, unmanagedThreadId);
                        AddEnvironment(env);
                    }
                }
                else
                {
                    isNew = false;
                    unmanagedThreadId = CurrentUnmanagedThreadId;
                }
                RecordLast(env);
                if (env != null && env.ThreadAffinity != unmanagedThreadId)
                {
                    throw new JniException("Unexpected - incorrect JNI Environment thread affinity.");
                }
                return env;
            }
        }

        private void RecordLast(JniEnvironment env)
        {
            mLastManagedThreadId = Thread.CurrentThread.ManagedThreadId;
            mLastJniEnv = new WeakReference(env);
        }

        internal static IntPtr GetEnvironmentHandle()
        {
            int unmanagedThreadId;
            bool isNew;
            return GetEnvironmentHandle(out unmanagedThreadId, out isNew);
        }

        private static IntPtr GetEnvironmentHandle(out int unmanagedThreadId, out bool isNew)
        {
            unmanagedThreadId = CurrentUnmanagedThreadId;
            isNew = false;
            IntPtr env = JavaVM.GetEnvironment();
            if (env == IntPtr.Zero) // environment not attached
            {
                string threadName = Thread.CurrentThread.Name;
                if (!(threadName != null && threadName.Length > 0))
                {
                    threadName = "{noname_thread}";
                }
                threadName += ", unmanaged thread ID: ";
                threadName += unmanagedThreadId.ToString(CultureInfo.InvariantCulture);
                JavaVMThreadAttachArguments args = new JavaVMThreadAttachArguments(threadName, null);
                env = JavaVM.AttachCurrentThread(args);
                isNew = true;
            }
            return env;
        }

        internal void HandleFinalization(JniEnvironment environment)
        {
            lock (mMapEnvironments)
            {
                mMapEnvironments.Remove(environment.Handle);
            }
        }

        private void AddEnvironment(JniEnvironment environment)
        {
            lock (mMapEnvironments)
            {
                mMapEnvironments[environment.Handle] = new WeakReference(environment);
                Thread.SetData(Thread.GetNamedDataSlot(JNIENV_TLS_NAME), environment);
            }
        }

        private JniEnvironment FromHandleCore(IntPtr environmentHandle)
        {
            lock (mMapEnvironments)
            {
                WeakReference weak;
                if (!mMapEnvironments.TryGetValue(environmentHandle, out weak))
                {
                    return null;
                }
                try
                {
                    return (JniEnvironment)weak.Target;
                }
                catch (InvalidOperationException)
                {
                    mMapEnvironments.Remove(environmentHandle);
                    return null;
                }
            }
        }
    }
}
